Repository navigation
feat(desktop,host-service): tombstone workspace deletes + kanban board view for the workspaces page - #6296
Conversation
Deleting a workspace now archives its row (archivedAt + archiveReason merged|deleted) at a commit point right after the teardown script; any later cleanup failure un-archives so the workspace stays retryable, and a startup reconciler finishes crash-interrupted deletes (guarded against worktree-path reuse). Sessions keep today's hard delete. workspace.list excludes archived rows unless the caller opts in; adopt/search/backfill/ watcher/PR sweeps all skip tombstones. pullRequests gains a sticky mergedAt stamped at first merged observation (migration 0020).
The workspaces page now keeps the host's full 5-valued PR state (incl. merge queue) instead of discarding it and re-deriving a 4-valued state from the cloud Electric join, which is removed; live agent status comes from one host-wide terminalAgents.list query per host. Archived tombstones ride a dedicated query key in useHostWorkspacesSource so the shared live cache and snapshots never see them. DeletingWorkspacesProvider is deleted — the durable archive mark replaces the in-memory in-flight set, and the delete dialog stays mounted via a per-row latch so the teardown-failure force-retry still re-opens.
…page Board/list toggle on the workspaces page: fixed status columns (Idle, Working, Needs attention, Needs review, Merged, Deleted) derived from agent status + PR state, ghosted archived cards, and a "Show archived" window dropdown (default past week). Filters become URL-synced: search, multi-select projects (with a Sessions sentinel), PR states, agent statuses, device, view mode, archived window — copy the URL, get the view.
📝 WalkthroughWalkthroughThis change adds workspace tombstones, archived-workspace retrieval, host-derived PR and agent status, a Kanban board, URL-synchronized filters, shared workspace actions, and Zustand-based deletion state. ChangesWorkspace tombstones and board
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant V2WorkspacesPage
participant AccessibleWorkspaces
participant HostWorkspaceSource
participant WorkspaceBoard
User->>V2WorkspacesPage: Select filters or board view
V2WorkspacesPage->>AccessibleWorkspaces: Request filtered workspaces
AccessibleWorkspaces->>HostWorkspaceSource: Load live and archived snapshots
HostWorkspaceSource-->>AccessibleWorkspaces: Return workspace, PR, and archive data
AccessibleWorkspaces-->>V2WorkspacesPage: Return accessible workspaces
V2WorkspacesPage->>WorkspaceBoard: Render grouped workspace cards
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (2)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/components/V2WorkspaceRow/V2WorkspaceRow.tsx (1)
69-71: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winUnmount the dialog after a canceled delete.
Line 71 latches
deleteDialogMountedpermanently. A normal close callsonOpenChange(false), but it never clears the latch. Each opened-and-canceled dialog then keeps its live-query subscription for the lifetime of the list.Clear the latch after an ordinary close. Keep it mounted only while a teardown operation can reopen the dialog.
Also applies to: 374-386
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/components/V2WorkspaceRow/V2WorkspaceRow.tsx` around lines 69 - 71, Update the V2WorkspaceRow delete-dialog lifecycle so deleteDialogMounted is cleared when onOpenChange receives a normal false close, releasing the dialog and its live-query subscription after cancellation. Preserve the latch only while teardown is in progress and may need to reopen the dialog, including the existing force-delete recovery path.apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardCard/V2WorkspacesBoardCard.tsx (1)
6-12: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMove
ChecksDotinto its own component file.
ChecksDotis a second React component in this file. Move it tocomponents/ChecksDot/ChecksDot.tsx, add itsindex.tsbarrel export, and import it here.As per coding guidelines, “Use one component per file and organize components as
ComponentName/ComponentName.tsxwith anindex.tsbarrel export.”Also applies to: 115-128
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardCard/V2WorkspacesBoardCard.tsx` around lines 6 - 12, Move the ChecksDot component out of V2WorkspacesBoardCard.tsx into components/ChecksDot/ChecksDot.tsx, add the corresponding index.ts barrel export, and import ChecksDot from that barrel in V2WorkspacesBoardCard.tsx while preserving its current behavior and props.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@apps/desktop/src/renderer/hooks/host-workspaces/useHostWorkspaces/useHostWorkspaces.ts`:
- Around line 275-283: Update the archived data mapping in useHostWorkspaces so
hostReachable reflects the corresponding archivedQueries[index] error state
instead of always being true. Reuse the same reachability derivation used by the
live-query path, while preserving the existing archived row filtering and
mapping behavior.
- Around line 173-196: Update the isReady calculation in useHostWorkspaces to
include settlement of every enabled query in archivedQueries when
includeArchived is true. Treat disabled archived queries as already settled,
preserve rendering of any live or archived data already available, and keep the
existing readiness behavior when archived data is not requested.
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts`:
- Around line 61-69: Update useDashboardSidebarShortcuts and its callers to
receive the in-flight deletion workspace IDs, then filter those IDs out of
flattenedWorkspaces before generating shortcut targets. Ensure
JUMP_TO_WORKSPACE_*, PREV_WORKSPACE, and NEXT_WORKSPACE cannot navigate to
workspaces pending deletion.
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/page.tsx`:
- Around line 95-143: Update the synchronization around hydratedRef and the
useEffect in the v2 workspaces route so changes to Route.useSearch() reapply all
URL filter values to useV2WorkspacesFilterStore before the store-to-URL
navigation can overwrite them, while preserving initial hydration behavior. Add
a route test covering same-route navigation between /v2-workspaces URLs with
different filters and verify the store and resulting URL remain synchronized.
In `@packages/host-service/src/runtime/archived-workspace-reconcile.ts`:
- Around line 35-44: Update the stranded-workspace teardown flow around
selectStranded and destroyWorkspace to revalidate ownership of each
row.worktreePath in the same serialized critical section as teardown. Prevent
force deletion when a live workspace claims the path between the initial
livePaths snapshot and cleanup, preferably by using an atomic path claim or a
cleanup guard inside destroyWorkspace; abort cleanup for paths owned by another
live row.
In `@packages/host-service/src/runtime/pull-requests/pull-requests.ts`:
- Around line 484-486: Update syncWorkspaceRow so its workspace update predicate
requires archivedAt IS NULL, preventing writes to archived tombstones even when
archival races with readWorkspaceRefs. Only refresh the project after the
conditional update reports success, and add a race test that archives the
workspace while readWorkspaceRefs is blocked.
In `@packages/host-service/src/trpc/router/project/project.ts`:
- Around line 842-853: Update the local workspace cleanup query in the project
deletion flow to include archived rows so their existing worktrees are removed
before the project cascade; remove the isNull(workspaces.archivedAt) filter
while preserving the projectId restriction. Add a test covering an archived
workspace with a still-existing worktree and verify it is cleaned up before
deletion.
In
`@packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts`:
- Around line 462-469: Update the phase label in the comment above the local
commit remainder logic near deleteLocalWorkspace to reference step 1.5 instead
of step 0.5, matching the archive phase documentation elsewhere.
- Around line 313-323: Update the rollback condition around marked,
archiveLocalWorkspace, and unarchiveLocalWorkspace so rollback occurs only when
the workspace row was live before this attempt. Capture whether local.archivedAt
was unset before archiving, use that state to gate the catch-block unarchive,
and preserve existing behavior for newly archived rows while never clearing a
pre-existing tombstone.
In `@packages/host-service/test/archived-workspace-reconcile.test.ts`:
- Around line 1-2: Move the unit test from the archived workspace reconciliation
test location to sit beside the source file containing selectStranded. Preserve
the existing test coverage and imports, adjusting only relative paths as needed
so the co-located test directly exercises selectStranded.
---
Nitpick comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardCard/V2WorkspacesBoardCard.tsx`:
- Around line 6-12: Move the ChecksDot component out of
V2WorkspacesBoardCard.tsx into components/ChecksDot/ChecksDot.tsx, add the
corresponding index.ts barrel export, and import ChecksDot from that barrel in
V2WorkspacesBoardCard.tsx while preserving its current behavior and props.
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/components/V2WorkspaceRow/V2WorkspaceRow.tsx`:
- Around line 69-71: Update the V2WorkspaceRow delete-dialog lifecycle so
deleteDialogMounted is cleared when onOpenChange receives a normal false close,
releasing the dialog and its live-query subscription after cancellation.
Preserve the latch only while teardown is in progress and may need to reopen the
dialog, including the existing force-delete recovery path.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 51509868-6f51-46e4-bbb9-b0310dd88375
📒 Files selected for processing (56)
apps/desktop/src/renderer/hooks/host-workspaces/useHostWorkspaces/useHostWorkspaces.tsapps/desktop/src/renderer/hooks/host-workspaces/useHostWorkspaces/useHostWorkspaces.utils.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarBulkDeleteDialog/hooks/useBulkWorkspaceDelete/useBulkWorkspaceDelete.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/useNavigateAwayFromWorkspace.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacePrHoverCardContent/V2WorkspacePrHoverCardContent.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacePrHoverCardContent/components/PrStateBadge/PrStateBadge.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/V2WorkspacesBoard.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardCard/V2WorkspacesBoardCard.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardCard/index.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardColumn/V2WorkspacesBoardColumn.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardColumn/index.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/index.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/utils/deriveBoardColumn/deriveBoardColumn.test.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/utils/deriveBoardColumn/deriveBoardColumn.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/utils/deriveBoardColumn/index.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesHeader/V2WorkspacesHeader.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesHeader/components/MultiSelectFilter/MultiSelectFilter.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesHeader/components/MultiSelectFilter/index.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesHeader/components/ProjectFilterTriggerLabel/ProjectFilterTriggerLabel.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesHeader/components/ProjectFilterTriggerLabel/index.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/V2WorkspacesList.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/components/V2WorkspaceRow/V2WorkspaceRow.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/hooks/useAccessibleV2Workspaces/useAccessibleV2Workspaces.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/page.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/stores/v2WorkspacesFilterStore/index.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/stores/v2WorkspacesFilterStore/v2WorkspacesFilterStore.tsapps/desktop/src/renderer/routes/_authenticated/layout.tsxapps/desktop/src/renderer/routes/_authenticated/providers/DeletingWorkspacesProvider/DeletingWorkspacesProvider.tsxapps/desktop/src/renderer/routes/_authenticated/providers/DeletingWorkspacesProvider/index.tspackages/host-service/drizzle/0020_overrated_dreaming_celestial.sqlpackages/host-service/drizzle/meta/0020_snapshot.jsonpackages/host-service/drizzle/meta/_journal.jsonpackages/host-service/src/app.tspackages/host-service/src/db/schema.tspackages/host-service/src/events/git-watcher.tspackages/host-service/src/runtime/archived-workspace-reconcile.tspackages/host-service/src/runtime/pull-requests/pull-requests.tspackages/host-service/src/runtime/workspace-backfill.tspackages/host-service/src/trpc/router/project/project.tspackages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.tspackages/host-service/src/trpc/router/workspace-creation/procedures/list-project-worktrees.tspackages/host-service/src/trpc/router/workspace-creation/procedures/search-branches.tspackages/host-service/src/trpc/router/workspace-creation/shared/adopt-existing-worktree.tspackages/host-service/src/trpc/router/workspace/workspace.tspackages/host-service/src/workspaces/local-workspace-store.tspackages/host-service/test/archived-workspace-reconcile.test.tspackages/host-service/test/integration/bug-hunt-2.integration.test.tspackages/host-service/test/integration/workspace-cleanup.integration.test.tspackages/host-service/test/integration/workspace-create-delete.integration.test.tspackages/host-service/test/integration/workspace.integration.test.tspackages/host-service/test/migration-0019-preserves-terminal-links.test.tspackages/host-service/test/workspace-cleanup.test.tsplans/done/20260809-1015-workspace-tombstones-kanban-board.md
💤 Files with no reviewable changes (6)
- apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesHeader/components/ProjectFilterTriggerLabel/index.ts
- apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesHeader/components/ProjectFilterTriggerLabel/ProjectFilterTriggerLabel.tsx
- apps/desktop/src/renderer/routes/_authenticated/providers/DeletingWorkspacesProvider/index.ts
- apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarBulkDeleteDialog/hooks/useBulkWorkspaceDelete/useBulkWorkspaceDelete.ts
- apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts
- apps/desktop/src/renderer/routes/_authenticated/providers/DeletingWorkspacesProvider/DeletingWorkspacesProvider.tsx
| import { describe, expect, test } from "bun:test"; | ||
| import { selectStranded } from "../src/runtime/archived-workspace-reconcile"; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Co-locate this unit test with selectStranded.
Move this test beside packages/host-service/src/runtime/archived-workspace-reconcile.ts. This test exercises one utility and is not a cross-cutting invariant test.
As per coding guidelines, “Co-locate tests with the component, hook, or utility they test.” Based on learnings, “follow the AGENTS.md rule to co-locate per-file unit tests (.test.ts) next to the source they test.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/host-service/test/archived-workspace-reconcile.test.ts` around lines
1 - 2, Move the unit test from the archived workspace reconciliation test
location to sit beside the source file containing selectStranded. Preserve the
existing test coverage and imports, adjusting only relative paths as needed so
the co-located test directly exercises selectStranded.
Sources: Coding guidelines, Learnings
🧹 Preview Cleanup CompleteThe following preview resources have been cleaned up:
Thank you for your contribution! 🎉 |
There was a problem hiding this comment.
7 issues found across 56 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/host-service/src/trpc/router/workspace-creation/shared/adopt-existing-worktree.ts">
<violation number="1" location="packages/host-service/src/trpc/router/workspace-creation/shared/adopt-existing-worktree.ts:123">
P1: Recreating a branch after deletion can return its archived workspace as an existing workspace, skipping adoption/creation. Filter tombstones from `findExistingWorkspaceByBranch` so this new live-only lookup behavior remains consistent through the earlier idempotency checks.</violation>
</file>
<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/components/V2WorkspaceRow/V2WorkspaceRow.tsx">
<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/components/V2WorkspaceRow/V2WorkspaceRow.tsx:378">
P3: Cancelling a delete permanently mounts this row’s delete-dialog hook tree; opening and cancelling dialogs across a large workspace list accumulates hidden components that rerender with shared host/preference context. Preserve the latch only while a destroy is in flight or a teardown failure must be recoverable, then clear it on an ordinary close.</violation>
</file>
<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesHeader/V2WorkspacesHeader.tsx">
<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesHeader/V2WorkspacesHeader.tsx:199">
P3: An active project filter loses its visible label when the search query excludes that project's options, so the trigger incorrectly says “All projects” although filtering remains active. Preserve selected projects from `projectsById` in the options/trigger label independent of search results.</violation>
</file>
<file name="packages/host-service/src/app.ts">
<violation number="1" location="packages/host-service/src/app.ts:258">
P0: A workspace recreated after the reconcile path snapshot can have its worktree deleted: the reconciler selects an archived row, creation registers a live row at that path, then the queued destroy removes that shared path. Revalidate live-path ownership atomically/immediately before each destroy (or serialize creation and reconciliation by path).</violation>
</file>
<file name="packages/host-service/src/runtime/pull-requests/pull-requests.ts">
<violation number="1" location="packages/host-service/src/runtime/pull-requests/pull-requests.ts:432">
P2: The periodic sweeps materialize every permanent tombstone before filtering, so refresh cost grows with deletion history. Filter `archived_at IS NULL` in SQL and update the test fakes to support the predicate.</violation>
</file>
<file name="packages/host-service/src/runtime/archived-workspace-reconcile.ts">
<violation number="1" location="packages/host-service/src/runtime/archived-workspace-reconcile.ts:26">
P1: A live workspace using a symlink-equivalent path can be treated as stranded and deleted at startup because `livePaths` compares raw strings while cleanup canonicalizes paths. Normalize both archived and live paths before ownership comparison.</violation>
<violation number="2" location="packages/host-service/src/runtime/archived-workspace-reconcile.ts:35">
P1: A user-created worktree reusing a permanent tombstone's path is deleted on the next host restart before it can be imported, since DB ownership is the only safety check. Reconcile only a checkout provably belonging to the interrupted delete, using a durable deletion marker/identity rather than path existence alone.</violation>
</file>
Architecture diagram
sequenceDiagram
participant Client as Desktop Renderer
participant HostSvc as Host Service (tRPC)
participant HostDB as Host SQLite
participant Bus as Event Bus
participant Recon as Startup Reconcile
Note over Client,HostSvc: Workspace Delete Flow (tombstoning)
Client->>HostSvc: destroy(workspaceId, options)
HostSvc->>HostSvc: preflight + run teardown script
alt non-session workspace
HostSvc->>HostDB: NEW: archive row (archivedAt, reason)
HostSvc->>HostSvc: physical cleanup (PTY, worktree, branch, cloud)
alt cleanup fails
HostSvc->>HostDB: NEW: unarchive row (revive)
HostSvc-->>Client: error
else cleanup succeeds
HostSvc-->>Client: success
end
HostSvc->>Bus: NEW: broadcast "deleted" (same shape as before)
else session workspace
HostSvc->>HostDB: hard delete (unchanged)
HostSvc->>Bus: broadcast "deleted"
end
Note over Recon,HostDB: Startup Reconcile (crash recovery)
Recon->>HostDB: select archived rows
Recon->>HostDB: select live worktree paths
Recon->>Recon: filter stranded (worktree exists + unowned)
loop each stranded row
Recon->>HostSvc: destroy(force=true, teardown=best-effort)
end
Note over Client,HostSvc: Board View Data Flow
Client->>HostSvc: workspace.list({includeArchived:true})
HostSvc->>HostDB: select live rows (default excludes archived)
HostSvc->>HostDB: select archived rows (opt-in)
HostSvc-->>Client: rows with archivedAt/reason + live data
Client->>HostSvc: terminalAgents.list (host-wide)
HostSvc-->>Client: agent statuses per terminal
Client->>HostSvc: getPullRequestsByWorkspaces (host PR snapshot)
HostSvc-->>Client: PR state (5-valued incl. queued), mergedAt
Client->>Client: deriveBoardColumn() → assign to column (NEW)
Note over Client: ghosted archived cards render with reason badge
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| }); | ||
| // Finish any delete the previous process crashed out of (archived row | ||
| // whose worktree still exists). | ||
| await runArchivedWorkspaceReconcile({ |
There was a problem hiding this comment.
P0: A workspace recreated after the reconcile path snapshot can have its worktree deleted: the reconciler selects an archived row, creation registers a live row at that path, then the queued destroy removes that shared path. Revalidate live-path ownership atomically/immediately before each destroy (or serialize creation and reconciliation by path).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/app.ts, line 258:
<comment>A workspace recreated after the reconcile path snapshot can have its worktree deleted: the reconciler selects an archived row, creation registers a live row at that path, then the queued destroy removes that shared path. Revalidate live-path ownership atomically/immediately before each destroy (or serialize creation and reconciliation by path).</comment>
<file context>
@@ -252,6 +253,23 @@ export function createApp(options: CreateAppOptions): CreateAppResult {
});
+ // Finish any delete the previous process crashed out of (archived row
+ // whose worktree still exists).
+ await runArchivedWorkspaceReconcile({
+ git,
+ credentials: providers.credentials,
</file context>
| where: and( | ||
| eq(workspaces.projectId, projectId), | ||
| eq(workspaces.branch, branch), | ||
| isNull(workspaces.archivedAt), |
There was a problem hiding this comment.
P1: Recreating a branch after deletion can return its archived workspace as an existing workspace, skipping adoption/creation. Filter tombstones from findExistingWorkspaceByBranch so this new live-only lookup behavior remains consistent through the earlier idempotency checks.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/trpc/router/workspace-creation/shared/adopt-existing-worktree.ts, line 123:
<comment>Recreating a branch after deletion can return its archived workspace as an existing workspace, skipping adoption/creation. Filter tombstones from `findExistingWorkspaceByBranch` so this new live-only lookup behavior remains consistent through the earlier idempotency checks.</comment>
<file context>
@@ -120,6 +120,7 @@ export async function adoptExistingWorktree(
where: and(
eq(workspaces.projectId, projectId),
eq(workspaces.branch, branch),
+ isNull(workspaces.archivedAt),
),
})
</file context>
| .all(); | ||
| if (archived.length === 0) return; | ||
|
|
||
| const livePaths = new Set( |
There was a problem hiding this comment.
P1: A live workspace using a symlink-equivalent path can be treated as stranded and deleted at startup because livePaths compares raw strings while cleanup canonicalizes paths. Normalize both archived and live paths before ownership comparison.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/runtime/archived-workspace-reconcile.ts, line 26:
<comment>A live workspace using a symlink-equivalent path can be treated as stranded and deleted at startup because `livePaths` compares raw strings while cleanup canonicalizes paths. Normalize both archived and live paths before ownership comparison.</comment>
<file context>
@@ -0,0 +1,73 @@
+ .all();
+ if (archived.length === 0) return;
+
+ const livePaths = new Set(
+ ctx.db
+ .select({ worktreePath: workspaces.worktreePath })
</file context>
| .map((row) => row.worktreePath), | ||
| ); | ||
|
|
||
| const stranded = selectStranded(archived, livePaths, existsSync); |
There was a problem hiding this comment.
P1: A user-created worktree reusing a permanent tombstone's path is deleted on the next host restart before it can be imported, since DB ownership is the only safety check. Reconcile only a checkout provably belonging to the interrupted delete, using a durable deletion marker/identity rather than path existence alone.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/runtime/archived-workspace-reconcile.ts, line 35:
<comment>A user-created worktree reusing a permanent tombstone's path is deleted on the next host restart before it can be imported, since DB ownership is the only safety check. Reconcile only a checkout provably belonging to the interrupted delete, using a durable deletion marker/identity rather than path existence alone.</comment>
<file context>
@@ -0,0 +1,73 @@
+ .map((row) => row.worktreePath),
+ );
+
+ const stranded = selectStranded(archived, livePaths, existsSync);
+
+ for (const row of stranded) {
</file context>
| only once the user opened it — not idle for every row. The latch | ||
| keeps it mounted through the destroy so a teardown-failure can | ||
| re-open it to offer force-delete. */} | ||
| {!isMainWorkspace && deleteDialogMounted ? ( |
There was a problem hiding this comment.
P3: Cancelling a delete permanently mounts this row’s delete-dialog hook tree; opening and cancelling dialogs across a large workspace list accumulates hidden components that rerender with shared host/preference context. Preserve the latch only while a destroy is in flight or a teardown failure must be recoverable, then clear it on an ordinary close.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/components/V2WorkspaceRow/V2WorkspaceRow.tsx, line 378:
<comment>Cancelling a delete permanently mounts this row’s delete-dialog hook tree; opening and cancelling dialogs across a large workspace list accumulates hidden components that rerender with shared host/preference context. Preserve the latch only while a destroy is in flight or a teardown failure must be recoverable, then clear it on an ordinary close.</comment>
<file context>
@@ -366,11 +371,11 @@ export function V2WorkspaceRow({
+ only once the user opened it — not idle for every row. The latch
+ keeps it mounted through the destroy so a teardown-failure can
+ re-open it to offer force-delete. */}
+ {!isMainWorkspace && deleteDialogMounted ? (
<DashboardSidebarDeleteDialog
workspaceId={workspace.id}
</file context>
| </SelectGroup> | ||
| <MultiSelectFilter | ||
| placeholder="All projects" | ||
| options={projectFilterOptions} |
There was a problem hiding this comment.
P3: An active project filter loses its visible label when the search query excludes that project's options, so the trigger incorrectly says “All projects” although filtering remains active. Preserve selected projects from projectsById in the options/trigger label independent of search results.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesHeader/V2WorkspacesHeader.tsx, line 199:
<comment>An active project filter loses its visible label when the search query excludes that project's options, so the trigger incorrectly says “All projects” although filtering remains active. Preserve selected projects from `projectsById` in the options/trigger label independent of search results.</comment>
<file context>
@@ -133,62 +194,43 @@ export function V2WorkspacesHeader({
- </SelectGroup>
+ <MultiSelectFilter
+ placeholder="All projects"
+ options={projectFilterOptions}
+ value={projectFilters}
+ onChange={setProjectFilters}
</file context>
Sessions are workspaces with a little missing data — no reason to exempt them from delete history. The pipeline loses its per-type branch at the commit point (reason is always "deleted"; sessions have no PR link), and sessions gain Deleted-column history on the board. Safe against path reuse: claimedSessionNames counts tombstones, so a tombstone's folder name is never handed to a new session and the reconciler's live-path guard holds. deleteLocalWorkspace remains only for adopt-conflict phantom-row cleanup.
|
Update: sessions now tombstone too (2c9e7eb). Original design exempted sessions (hard delete); reversed after review — they're essentially workspaces with a little missing data. Reason is always Delete-flow verification (CDP, real UI clicks on the dev app):
|
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/host-service/test/workspace-cleanup.test.ts (1)
708-708: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCo-locate this unit test with the workspace cleanup router.
packages/host-service/test/workspace-cleanup.test.tstests one router module but is stored in the package-level test directory. Move it besidepackages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts. Keep integration tests underpackages/host-service/test/integration/.As per coding guidelines, tests must be co-located with the code they use. Based on learnings, host-service per-file unit tests are co-located, while cross-cutting integration tests remain under
test/integration.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/host-service/test/workspace-cleanup.test.ts` at line 708, Move the unit tests from workspace-cleanup.test.ts beside the workspace-cleanup router implementation, preserving their contents and test behavior. Keep only cross-cutting integration tests under the host-service test/integration directory.Sources: Coding guidelines, Learnings
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/host-service/test/workspace-cleanup.test.ts`:
- Line 708: Move the unit tests from workspace-cleanup.test.ts beside the
workspace-cleanup router implementation, preserving their contents and test
behavior. Keep only cross-cutting integration tests under the host-service
test/integration directory.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 86cc56e8-2762-4b05-bc3b-08a233bfb79f
📒 Files selected for processing (7)
packages/host-service/src/db/schema.tspackages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.tspackages/host-service/src/trpc/router/workspace-creation/procedures/create-session.tspackages/host-service/src/workspaces/local-workspace-store.tspackages/host-service/test/integration/session-create-delete.integration.test.tspackages/host-service/test/workspace-cleanup.test.tsplans/done/20260809-1015-workspace-tombstones-kanban-board.md
🚧 Files skipped from review as they are similar to previous changes (3)
- packages/host-service/src/workspaces/local-workspace-store.ts
- packages/host-service/src/db/schema.ts
- packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts
There was a problem hiding this comment.
2 issues found across 7 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts">
<violation number="1" location="packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts:315">
P2: A corrupt session path outside the managed root now reports deletion success while leaving its folder and an archived, name-claiming tombstone; treat the skipped folder removal as a cleanup failure so the outer handler unarchives it for retry.</violation>
</file>
<file name="plans/done/20260809-1015-workspace-tombstones-kanban-board.md">
<violation number="1" location="plans/done/20260809-1015-workspace-tombstones-kanban-board.md:87">
P3: The new decision-log entry reverses the session exemption (sessions now tombstone on delete), but the rest of this plan still documents the opposite: Big Picture ("Sessions... are exempt and keep today's hard delete"), Milestone 2 steps 1/3 ("for non-session workspaces only" / "for sessions keep deleteLocalWorkspace (hard delete, unchanged)"), the Tests and Validation checklists ("session destroy still hard-deletes the row"), and Outcomes ("sessions still hard-delete"). A reader reconciling the reversed decision against the plan's operational steps will get conflicting instructions about the destroy pipeline. Please propagate the reversal to the affected sections so the plan is internally consistent.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| // a little missing data (no project, no PRs; reason is always | ||
| // "deleted"), and session folder names are claimed against ALL rows | ||
| // including tombstones, so a tombstone's path can't be reused. | ||
| const marked = local != null; |
There was a problem hiding this comment.
P2: A corrupt session path outside the managed root now reports deletion success while leaving its folder and an archived, name-claiming tombstone; treat the skipped folder removal as a cleanup failure so the outer handler unarchives it for retry.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts, line 315:
<comment>A corrupt session path outside the managed root now reports deletion success while leaving its folder and an archived, name-claiming tombstone; treat the skipped folder removal as a cleanup failure so the outer handler unarchives it for retry.</comment>
<file context>
@@ -309,8 +308,11 @@ async function runDestroy(
+ // a little missing data (no project, no PRs; reason is always
+ // "deleted"), and session folder names are claimed against ALL rows
+ // including tombstones, so a tombstone's path can't be reused.
+ const marked = local != null;
if (marked) {
archiveLocalWorkspace(ctx, input.workspaceId, archiveReasonFor(ctx, local));
</file context>
| - Decision: `V2WorkspaceRow` keeps its delete dialog mounted via a "mounted once opened" latch instead of the deleted provider's `deleting` flag. | ||
| Rationale: preserves the teardown-failure re-open path without any global in-flight registry. | ||
| Date/Author: 2026-08-09, implementation. | ||
| - Decision: REVERSAL of the session exemption — sessions tombstone on delete too (reason always "deleted"; no PR link exists). |
There was a problem hiding this comment.
P3: The new decision-log entry reverses the session exemption (sessions now tombstone on delete), but the rest of this plan still documents the opposite: Big Picture ("Sessions... are exempt and keep today's hard delete"), Milestone 2 steps 1/3 ("for non-session workspaces only" / "for sessions keep deleteLocalWorkspace (hard delete, unchanged)"), the Tests and Validation checklists ("session destroy still hard-deletes the row"), and Outcomes ("sessions still hard-delete"). A reader reconciling the reversed decision against the plan's operational steps will get conflicting instructions about the destroy pipeline. Please propagate the reversal to the affected sections so the plan is internally consistent.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At plans/done/20260809-1015-workspace-tombstones-kanban-board.md, line 87:
<comment>The new decision-log entry reverses the session exemption (sessions now tombstone on delete), but the rest of this plan still documents the opposite: Big Picture ("Sessions... are exempt and keep today's hard delete"), Milestone 2 steps 1/3 ("for non-session workspaces only" / "for sessions keep deleteLocalWorkspace (hard delete, unchanged)"), the Tests and Validation checklists ("session destroy still hard-deletes the row"), and Outcomes ("sessions still hard-delete"). A reader reconciling the reversed decision against the plan's operational steps will get conflicting instructions about the destroy pipeline. Please propagate the reversal to the affected sections so the plan is internally consistent.</comment>
<file context>
@@ -84,6 +84,9 @@ All decisions below were made with Kiet in a walkthrough on 2026-08-09.
- Decision: `V2WorkspaceRow` keeps its delete dialog mounted via a "mounted once opened" latch instead of the deleted provider's `deleting` flag.
Rationale: preserves the teardown-failure re-open path without any global in-flight registry.
Date/Author: 2026-08-09, implementation.
+- Decision: REVERSAL of the session exemption — sessions tombstone on delete too (reason always "deleted"; no PR link exists).
+ Rationale: Kiet, post-review: "they're essentially workspaces with a little missing data". Also simplifies the pipeline (no per-type branch at the commit point) and gives sessions Deleted-column history. Safe because `claimedSessionNames` counts tombstones, so a tombstone's folder path is never reused and the reconciler's live-path guard holds.
+ Date/Author: 2026-08-09, Kiet.
</file context>
Extract the list row's right-click menu (Open, Copy Branch Name, Pin/Unpin, Delete) plus its delete-dialog latch into a shared V2WorkspaceContextMenu render-prop component and wrap board cards in it. The render prop hands the same actions to inline affordances, so the card's click-to-open drops its duplicated paywall gating and the row's pin/trash buttons reuse one implementation. Archived ghost cards render without the menu — no actions apply to a tombstone.
|
Board cards now share the list row's right-click menu (2194546). Extracted the row's context menu + delete-dialog latch into CDP-verified live: identical menu items on row and card (Open · Copy Branch Name · Unpin from Sidebar · Delete), menu actions fire from a card, and a main workspace's card correctly omits Delete. |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardCard/V2WorkspacesBoardCard.tsx (1)
123-136: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winAdd an accessible name for each check status.
ChecksDotrenders status icons without text or an ARIA label. Add accessible labels for pending, successful, and failed checks. Render a visually hidden “No checks” label whenstatus === "none"instead of returning an unlabeled absence.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardCard/V2WorkspacesBoardCard.tsx` around lines 123 - 136, Update ChecksDot so every status has an accessible name: render a visually hidden “No checks” label for “none”, and add visually hidden labels identifying pending, successful, and failed checks alongside their icons. Preserve the existing icon and styling for each non-none status.
🧹 Nitpick comments (1)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardCard/V2WorkspacesBoardCard.tsx (1)
41-50: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMove the helper components into separate component files.
V2WorkspacesBoardCard.tsxdefinesV2WorkspacesBoardCard,BoardCardBody, andChecksDot. Move each helper into its ownComponentName/ComponentName.tsxfile and add the correspondingindex.tsbarrel export. Runbun run lint:fix, then verify thatbun run lintexits successfully.As per coding guidelines, use one component per file and organize components as
ComponentName/ComponentName.tsxwith anindex.tsbarrel export.Also applies to: 123-136
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardCard/V2WorkspacesBoardCard.tsx` around lines 41 - 50, Split V2WorkspacesBoardCard, BoardCardBody, and ChecksDot into separate ComponentName/ComponentName.tsx files, preserving their existing behavior and imports; add an index.ts barrel export for each component and update consumers/imports accordingly. Run bun run lint:fix, then confirm bun run lint succeeds.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardCard/V2WorkspacesBoardCard.tsx`:
- Around line 123-136: Update ChecksDot so every status has an accessible name:
render a visually hidden “No checks” label for “none”, and add visually hidden
labels identifying pending, successful, and failed checks alongside their icons.
Preserve the existing icon and styling for each non-none status.
---
Nitpick comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardCard/V2WorkspacesBoardCard.tsx`:
- Around line 41-50: Split V2WorkspacesBoardCard, BoardCardBody, and ChecksDot
into separate ComponentName/ComponentName.tsx files, preserving their existing
behavior and imports; add an index.ts barrel export for each component and
update consumers/imports accordingly. Run bun run lint:fix, then confirm bun run
lint succeeds.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: e41752af-d44c-48ab-b772-ca6ecc8d1fe9
📒 Files selected for processing (4)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspaceContextMenu/V2WorkspaceContextMenu.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspaceContextMenu/index.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardCard/V2WorkspacesBoardCard.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/components/V2WorkspaceRow/V2WorkspaceRow.tsx
Renderer: URL hydration now applies only params the URL carries, so a bare sidebar navigation no longer resets board view and filters; the empty-dashboard gate counts live rows, not tombstones; archived polling runs only in board view; multi-select options key cmdk by unique id (labels stay searchable via keywords); a module-scope deletingWorkspaces store excludes mid-teardown workspaces from navigation targets and keyboard shortcuts (the archive commit lands after slow teardown, so rows stay listed for seconds). Host: the reconciler re-checks live path ownership immediately before each destroy; refresh-driven workspace updates carry archivedAt IS NULL guards so an archive mid-await can't mutate a frozen tombstone; project.remove sweeps stranded tombstone worktrees before the cascade erases them; deletion telemetry fires at the true commit (failed destroys don't count, retries count once); tombstones skip the per-poll existsSync; archive-reason lookup failures log. Also: archived-query settlement counts toward isReady, archived rows report honest hostReachable, shared WorkspaceChecksDot, reconcile test co-located, dead delete-throw test plumbing removed.
|
Review pass complete (773df8b). Ran a high-effort multi-agent review over the full diff plus triage of all 27 CodeRabbit/cubic comments. Fixed:
Skipped with reasoning:
|
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/page.tsx (1)
184-191: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winDo not replace an archived-only board with the create screen.
In board mode,
allincludes archived workspaces. If every live workspace was deleted,liveWorkspaces.length === 0is true even when the board has archived ghost cards to render. The empty-screen experiment then hides the Deleted or Merged columns.Use
all.length === 0for this condition, or exclude board mode from the create-screen path.Proposed fix
- liveWorkspaces.length === 0 && + all.length === 0 &&🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/page.tsx` around lines 184 - 191, Update the isEmptyDashboard condition in the dashboard page to use all.length === 0 instead of liveWorkspaces.length === 0, so archived-only boards continue rendering their Deleted or Merged columns while genuinely empty dashboards still show the create screen.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/host-service/src/trpc/router/project/project.ts`:
- Around line 843-853: Prevent duplicate workspace deletion telemetry during
project removal: in the cleanup loop handling localWorkspaces, ensure archived
rows with surviving worktrees are purged without invoking the telemetry-emitting
deleteLocalWorkspace path, or suppress telemetry when archivedAt is non-null
while preserving normal cleanup for live rows. Add a regression test covering
removal of a project containing an archived workspace whose worktree still
exists.
---
Outside diff comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/page.tsx`:
- Around line 184-191: Update the isEmptyDashboard condition in the dashboard
page to use all.length === 0 instead of liveWorkspaces.length === 0, so
archived-only boards continue rendering their Deleted or Merged columns while
genuinely empty dashboards still show the create screen.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 92055fdc-0bc9-4293-b158-05c44609c9ee
📒 Files selected for processing (22)
apps/desktop/src/renderer/hooks/host-workspaces/useHostWorkspaces/useHostWorkspaces.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarBulkDeleteDialog/hooks/useBulkWorkspaceDelete/useBulkWorkspaceDelete.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/useNavigateAwayFromWorkspace.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/stores/deletingWorkspacesStore/deletingWorkspacesStore.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/stores/deletingWorkspacesStore/index.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardCard/V2WorkspacesBoardCard.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesHeader/components/MultiSelectFilter/MultiSelectFilter.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/components/V2WorkspaceRow/V2WorkspaceRow.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/WorkspaceChecksDot/WorkspaceChecksDot.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/WorkspaceChecksDot/index.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/page.tsxpackages/host-service/src/runtime/archived-workspace-reconcile.test.tspackages/host-service/src/runtime/archived-workspace-reconcile.tspackages/host-service/src/runtime/pull-requests/pull-requests.tspackages/host-service/src/trpc/router/project/project.tspackages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.tspackages/host-service/src/trpc/router/workspace/workspace.tspackages/host-service/src/workspaces/local-workspace-store.tspackages/host-service/test/workspace-cleanup.test.tsplans/done/20260809-1015-workspace-tombstones-kanban-board.md
🚧 Files skipped from review as they are similar to previous changes (8)
- packages/host-service/src/trpc/router/workspace/workspace.ts
- packages/host-service/src/runtime/archived-workspace-reconcile.ts
- apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesBoard/components/V2WorkspacesBoardCard/V2WorkspacesBoardCard.tsx
- apps/desktop/src/renderer/hooks/host-workspaces/useHostWorkspaces/useHostWorkspaces.ts
- apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesHeader/components/MultiSelectFilter/MultiSelectFilter.tsx
- packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts
- packages/host-service/src/runtime/pull-requests/pull-requests.ts
- plans/done/20260809-1015-workspace-tombstones-kanban-board.md
| // The project-row delete below cascades tombstones away — removing a | ||
| // project intentionally drops its workspace history. Sweep worktrees | ||
| // for live rows AND stranded tombstones (crash-interrupted deletes | ||
| // whose worktree survives): once the cascade runs, the startup | ||
| // reconciler can no longer see them. | ||
| const localWorkspaces = ctx.db | ||
| .select() | ||
| .from(workspaces) | ||
| .where(eq(workspaces.projectId, input.projectId)) | ||
| .all(); | ||
| .all() | ||
| .filter((ws) => ws.archivedAt == null || existsSync(ws.worktreePath)); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Avoid duplicate deletion telemetry for archived rows.
This filter includes archived rows with existing worktrees. The later cleanup loop passes those rows to deleteLocalWorkspace at Line 871. That helper emits workspace_deleted telemetry at Line 216. The destroy workflow already calls trackWorkspaceDeleted after successful cleanup. An archived workspace with a surviving path can therefore produce two deletion events.
Use a no-telemetry purge for archived rows, or skip telemetry when the existing row has a non-null archivedAt. Add a regression test for project removal of an archived workspace with an existing worktree.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/host-service/src/trpc/router/project/project.ts` around lines 843 -
853, Prevent duplicate workspace deletion telemetry during project removal: in
the cleanup loop handling localWorkspaces, ensure archived rows with surviving
worktrees are purged without invoking the telemetry-emitting
deleteLocalWorkspace path, or suppress telemetry when archivedAt is non-null
while preserving normal cleanup for live rows. Add a regression test covering
removal of a project containing an archived workspace whose worktree still
exists.
There was a problem hiding this comment.
5 issues found across 22 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/stores/deletingWorkspacesStore/deletingWorkspacesStore.ts">
<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/stores/deletingWorkspacesStore/deletingWorkspacesStore.ts:12">
P2: Overlapping delete flows can clear the shared ID before teardown finishes, re-enabling shortcuts/navigation to a workspace still being destroyed. Track an in-flight count per ID (or ownership tokens) and remove it only when the final flow settles.</violation>
</file>
<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts">
<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts:73">
P1: Prev/next shortcuts can still navigate into a workspace mid-destroy because the workspace page's concurrent handlers bypass this filtered list. Have those handlers consult `deletingIds` (or consolidate the bindings) so every shortcut target excludes in-flight deletes.</violation>
</file>
<file name="packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts">
<violation number="1" location="packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts:329">
P2: A repeated destroy of an already archived workspace emits `workspace_deleted` again, contradicting the once-per-delete telemetry contract. Gate tracking on `local.archivedAt === null` from the pre-archive row so completed retries do not double-count.</violation>
</file>
<file name="packages/host-service/src/trpc/router/project/project.ts">
<violation number="1" location="packages/host-service/src/trpc/router/project/project.ts:853">
P2: Deleting a project leaves archived cards whose worktree is already gone visible on an open board for up to 30 seconds, because these rows are cascade-deleted without a `workspace:changed` event. Keep all project rows for the per-row `deleteLocalWorkspace` loop, while using a separate filtered set only for Git worktree cleanup.</violation>
</file>
<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/page.tsx">
<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/page.tsx:126">
P1: Copied URLs that omit default filters do not reproduce the sender's view when the receiver has prior non-default store state; e.g. `?view=board` retains `archived=all` instead of the sender's default week. Encode defaults in shareable URLs or add an explicit navigation signal that resets omitted fields for deep links while preserving bare sidebar navigation.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| [groups, sessionWorkspaces, isDeleting], | ||
| // A destroy in flight keeps its row until the archive commit — | ||
| // don't hand shortcuts a workspace that is about to vanish. | ||
| ].filter((workspace) => !deletingIds.has(workspace.id)), |
There was a problem hiding this comment.
P1: Prev/next shortcuts can still navigate into a workspace mid-destroy because the workspace page's concurrent handlers bypass this filtered list. Have those handlers consult deletingIds (or consolidate the bindings) so every shortcut target excludes in-flight deletes.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts, line 73:
<comment>Prev/next shortcuts can still navigate into a workspace mid-destroy because the workspace page's concurrent handlers bypass this filtered list. Have those handlers consult `deletingIds` (or consolidate the bindings) so every shortcut target excludes in-flight deletes.</comment>
<file context>
@@ -58,15 +59,19 @@ export function useDashboardSidebarShortcuts(
+ ),
+ // A destroy in flight keeps its row until the archive commit —
+ // don't hand shortcuts a workspace that is about to vanish.
+ ].filter((workspace) => !deletingIds.has(workspace.id)),
+ [groups, sessionWorkspaces, deletingIds],
);
</file context>
| ...(search.view !== undefined && { | ||
| viewMode: "board" as V2WorkspacesViewMode, | ||
| }), | ||
| ...(search.archived !== undefined && { |
There was a problem hiding this comment.
P1: Copied URLs that omit default filters do not reproduce the sender's view when the receiver has prior non-default store state; e.g. ?view=board retains archived=all instead of the sender's default week. Encode defaults in shareable URLs or add an explicit navigation signal that resets omitted fields for deep links while preserving bare sidebar navigation.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/page.tsx, line 126:
<comment>Copied URLs that omit default filters do not reproduce the sender's view when the receiver has prior non-default store state; e.g. `?view=board` retains `archived=all` instead of the sender's default week. Encode defaults in shareable URLs or add an explicit navigation signal that resets omitted fields for deep links while preserving bare sidebar navigation.</comment>
<file context>
@@ -92,31 +92,45 @@ function V2WorkspacesPage() {
+ ...(search.view !== undefined && {
+ viewMode: "board" as V2WorkspacesViewMode,
+ }),
+ ...(search.archived !== undefined && {
+ archivedWindow: search.archived,
+ }),
</file context>
| * by the destroy flows and read wherever a target is picked. | ||
| */ | ||
| interface DeletingWorkspacesState { | ||
| deletingIds: ReadonlySet<string>; |
There was a problem hiding this comment.
P2: Overlapping delete flows can clear the shared ID before teardown finishes, re-enabling shortcuts/navigation to a workspace still being destroyed. Track an in-flight count per ID (or ownership tokens) and remove it only when the final flow settles.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/stores/deletingWorkspacesStore/deletingWorkspacesStore.ts, line 12:
<comment>Overlapping delete flows can clear the shared ID before teardown finishes, re-enabling shortcuts/navigation to a workspace still being destroyed. Track an in-flight count per ID (or ownership tokens) and remove it only when the final flow settles.</comment>
<file context>
@@ -0,0 +1,33 @@
+ * by the destroy flows and read wherever a target is picked.
+ */
+interface DeletingWorkspacesState {
+ deletingIds: ReadonlySet<string>;
+ markDeleting: (workspaceId: string) => void;
+ clearDeleting: (workspaceId: string) => void;
</file context>
| }); | ||
| // Telemetry at the true commit: a failed destroy un-archives below and | ||
| // must not count, and a retried destroy must count exactly once. | ||
| if (marked && local) trackWorkspaceDeleted(ctx, local); |
There was a problem hiding this comment.
P2: A repeated destroy of an already archived workspace emits workspace_deleted again, contradicting the once-per-delete telemetry contract. Gate tracking on local.archivedAt === null from the pre-archive row so completed retries do not double-count.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts, line 329:
<comment>A repeated destroy of an already archived workspace emits `workspace_deleted` again, contradicting the once-per-delete telemetry contract. Gate tracking on `local.archivedAt === null` from the pre-archive row so completed retries do not double-count.</comment>
<file context>
@@ -318,7 +319,15 @@ async function runDestroy(
+ });
+ // Telemetry at the true commit: a failed destroy un-archives below and
+ // must not count, and a retried destroy must count exactly once.
+ if (marked && local) trackWorkspaceDeleted(ctx, local);
+ return result;
} catch (err) {
</file context>
| if (marked && local) trackWorkspaceDeleted(ctx, local); | |
| if (marked && local && local.archivedAt === null) trackWorkspaceDeleted(ctx, local); |
| .where(eq(workspaces.projectId, input.projectId)) | ||
| .all(); | ||
| .all() | ||
| .filter((ws) => ws.archivedAt == null || existsSync(ws.worktreePath)); |
There was a problem hiding this comment.
P2: Deleting a project leaves archived cards whose worktree is already gone visible on an open board for up to 30 seconds, because these rows are cascade-deleted without a workspace:changed event. Keep all project rows for the per-row deleteLocalWorkspace loop, while using a separate filtered set only for Git worktree cleanup.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/trpc/router/project/project.ts, line 853:
<comment>Deleting a project leaves archived cards whose worktree is already gone visible on an open board for up to 30 seconds, because these rows are cascade-deleted without a `workspace:changed` event. Keep all project rows for the per-row `deleteLocalWorkspace` loop, while using a separate filtered set only for Git worktree cleanup.</comment>
<file context>
@@ -839,19 +840,17 @@ export const projectRouter = router({
- .all();
+ .where(eq(workspaces.projectId, input.projectId))
+ .all()
+ .filter((ws) => ws.archivedAt == null || existsSync(ws.worktreePath));
for (const ws of localWorkspaces) {
</file context>
* docs: weekly update for PRs merged 2026-08-05 to 2026-08-12 - workspaces: sessions (project-less workspaces, #6274), pin + drag-and-drop pinning (#6364), list/board views with Merged & Deleted history columns (#6296), per-project naming instructions (#6250), multi-PR links per branch with remove/restore (#5924) - automations: session automations (#6280), list health stats + Up next grouping + read-only team automations (#6291, #6276), detail page Settings/Run History tabs, inline run errors, would-run preview, RRule validation (#6305) - editor: image pane pan/pinch/zoom (#6285) - tasks: multi-repo filters, expanded task states, PR author/review filters, PR checks (#6201) - monorepos: per-project sparse checkout (#6034) - customization: project icon + color picker, v1 color carry-over (#6249) - terminal presets: built-in Superset CLI preset (#6270) - agents: resume interrupted sessions + Resume Args setting (#6253, #6265), Superset hooks toggle + scoping (#6072) - cli: interactive command browser + branded help (#6273); reference updated for session mode, --task, --skip-branch-prefix, --session (verified against packages/cli/src/commands) * docs: tighten wording and drop em dashes Shorten this week's additions per the writing guide: one thought per sentence, plainest word, no em dashes as prose punctuation. Also sweeps the three pre-existing em dashes in recipes. * docs: add screenshots from the 08-09 changelog batch cli command browser, agent resume banner, PR view with checks, and the project icon/color picker. Copied into apps/docs/public/images per the SOP rather than referenced across apps.
…create (#6552) * fix(host-service): don't reuse a tombstoned workspace on same-branch create Since #6296 turned workspace deletes into tombstones (archivedAt), the create flow's branch-idempotency lookup still matched archived rows, so recreating a deleted workspace's name returned the dead row: no worktree was created, the call reported alreadyExists, and the desktop navigated to an id the workspace list filters out — landing on "Workspace not found" (#6383, #6380). Filter tombstones out of findExistingWorkspaceByBranch, matching the isNull(archivedAt) convention every sibling lookup already follows, and add an integration regression test for the delete → same-branch recreate flow. * docs(host-service): spell out the tombstone no-op symptom in the idempotency filter Folds in the clearer rationale from the duplicate PR #6605: matching a tombstone returns the archived row with alreadyExists: true and silently skips the create, and the adopt path already filters archived rows. --------- Co-authored-by: Kiet Ho <hoakiet98@gmail.com>
Links
plans/done/20260809-1015-workspace-tombstones-kanban-board.md(decision log + deviations recorded there)Summary
This PR bundles two related features, decided in a design walkthrough with Kiet:
archivedAt+archiveReason: "merged" | "deleted") instead of erasing it. Sessions tombstone too (reason is always"deleted"— they have no PR link).Part 1: Tombstoning (host-service)
Why
Merged and deleted workspaces vanished without a trace — the row delete was documented as "the commit point and the only record." History is the raw material for the board's Merged/Deleted columns.
What / How
workspacesgainsarchivedAt/archiveReason;pullRequestsgains a stickymergedAt(migration0020, additive only, generated with drizzle-kit — not hand-edited)."merged"when the linked PR was observed merged, else"deleted". Any later cleanup failure un-archives so the workspace stays visible and retryable; the archive broadcast reuses the existingdeletedevent shape so no consumer needs a wire migration.runArchivedWorkspaceReconcile(startup sweep) finishes crash-interrupted deletes — archived rows whose worktree still exists — with force + best-effort teardown, guarded so it never touches a path a live row owns (worktree paths get reused when a branch is re-created).workspace.listexcludes archived rows by default;includeArchived: trueopts in. Adopt-existing-worktree, branch search, worktree re-import, name backfill, the git watcher, and all PR refresh sweeps skip tombstones;getPullRequestsByWorkspacesdeliberately still serves archived ids so ghost cards keep their frozen PR pill.Key Decisions
worktreePath/branchare unindexed; one-main-per-project can't archive)Part 2: Board + filters (desktop)
Why
The page was a flat table with a substring search and two single-select dropdowns — no way to see work by state. Linear's board-is-a-layout model (researched against Linear's docs/Mobbin) fits: columns are derived state, filters are layout-independent, done items stay accessible without cluttering.
What / How
queued) instead of discarding it and re-deriving a worse 4-valued state from the cloud ElectricgithubPullRequestsjoin, which is removed (local-first; also fixes sessions never matching). Live agent status comes from one host-wideterminalAgents.listquery per host — never per workspace. Archived tombstones ride a dedicated query key inuseHostWorkspacesSourceso the shared live cache and its IndexedDB snapshots never see them.deriveBoardColumn, first match wins: archived reason → live merged PR → permission/failed → working → review/open PR → idle). Archived cards render ghosted with a reason badge and are non-clickable. No drag-and-drop in v1 (columns are derived state; nothing settable to drag to). "Show archived: none / past week / past month / all" (default past week) scopes tombstones.validateSearchon the route —q,device,projects(multi,__sessions__sentinel),pr,agent,view,archived. URL hydrates the store once per mount, then the store drives the URL (replace). Copying the URL reproduces the exact view, including board mode.DeletingWorkspacesProviderdeleted: the durable archive mark replaces the renderer-memory in-flight set (which other devices never saw anyway). The v2 row's delete dialog stays mounted via a mounted-once-opened latch so the teardown-failure force-retry still re-opens.Manual QA Checklist (all verified via CDP on the dev app from this worktree, screenshots captured)
Tombstoning
deleted; worktree removed, row kept (verified in host.db)merged0020applied cleanly to an existing dev host.db on bootBoard / filters
?view=board?pr=merged&view=boardlocation.reload()on a filtered URL restores board mode + active filters from the URL alonequeuedPR state renders (amber) end-to-end now that host state is usedNot manually exercised (covered by tests / low-risk reasoning)
Testing
bun run typecheck— clean, all packagesbun run lint— exits 0packages/host-service:bun test— 1072 pass / 0 fail (new: archive semantics integration tests, merged-reason test, session tombstone tests, reconciler stranded-selection tests, commit-point failure tests)apps/desktop:bun test— 2630 pass / 0 fail (new:deriveBoardColumnprecedence tests)Known Limitations
Follow-ups (deliberately out of scope)
mergedAtRollout / Rollback
delete from workspaces where archived_at is not null.Summary by cubic
Soft-delete all workspaces (including sessions) in
packages/host-serviceand add a Kanban board view with URL‑synced filters inapps/desktop. This update also tightens safety and UX: archived rows are fetched only for the board, navigation ignores in‑flight deletes, and the reconciler adds extra guards.New Features
archivedAt/archiveReason("merged"or"deleted"); sessions also tombstone ("deleted"); PRs get stickymergedAt.archivedAt IS NULL; project removal sweeps stranded tombstone worktrees; deletion telemetry fires at the true commit.workspace.listexcludes tombstones by default (includeArchived: trueto opt in); creation/search/backfill/watchers skip archived; tombstones fetched under a separate query key; archived polling runs only in board view; archived rows report honesthostReachable.deletingWorkspacesstore; removedDeletingWorkspacesProvider.Migration
workspaces.archived_at,workspaces.archive_reason, andpull_requests.merged_at(additive, forward‑only; no backfill).Written for commit 773df8b. Summary will update on new commits.
Summary by CodeRabbit